Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 55f718c72b76013892822692b66d45cff10b71ea


Parents : 37d4b31
Author : Sudo-Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-01-07T19:13:35-06:00

feat(frontend): enhance link rendering and markdown processing

- Added LinkUtils for detecting and rendering NomadNet and standard links in text.
- Introduced MarkdownRenderer for converting Markdown to HTML, including support for code blocks, headers, and inline formatting.
- Implemented escapeHtml utility function to prevent XSS in rendered text.
- Updated ToastUtils to support an optional key parameter for toast notifications.
- Included Italian language support in the frontend localization.

Changes
Diff

diff --git a/electron/main.js b/electron/main.js
index 4cdd531a..59b8cc3c 100644
--- a/electron/main.js
+++ b/electron/main.js
@@ -348,9 +348,9 @@ app.whenReady().then(async () => {
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
"style-src 'self' 'unsafe-inline'",
- "img-src 'self' data: blob: https://*.tile.openstreetmap.org https://tile.openstreetmap.org",
+ "img-src 'self' data: blob: https://*.tile.openstreetmap.org https://tile.openstreetmap.org https://*.cartocdn.com",
"font-src 'self' data:",
- "connect-src 'self' http://localhost:9337 https://localhost:9337 ws://localhost:* wss://localhost:* blob: https://*.tile.openstreetmap.org https://tile.openstreetmap.org https://nominatim.openstreetmap.org https://git.quad4.io",
+ "connect-src 'self' http://localhost:9337 https://localhost:9337 ws://localhost:* wss://localhost:* blob: https://*.tile.openstreetmap.org https://tile.openstreetmap.org https://nominatim.openstreetmap.org https://git.quad4.io https://*.cartocdn.com",
"media-src 'self' blob:",
"worker-src 'self' blob:",
"frame-src 'self'",

diff --git a/meshchatx/src/frontend/js/LinkUtils.js b/meshchatx/src/frontend/js/LinkUtils.js
new file mode 100644
index 00000000..c19a80c8
--- /dev/null
+++ b/meshchatx/src/frontend/js/LinkUtils.js
@@ -0,0 +1,41 @@
+export default class LinkUtils {
+ /**
+ * Detects and wraps NomadNet links in HTML.
+ * Supports nomadnet://<hash>:/path and <hash>:/path
+ */
+ static renderNomadNetLinks(text) {
+ if (!text) return "";
+
+ // Hash is 32 hex chars. Path is optional.
+ const hashPattern = "[a-fA-F0-9]{32}";
+ const nomadnetRegex = new RegExp(`(?:nomadnet://)?(${hashPattern})(?::(/[\\w\\d./?%&=-]*))?`, "g");
+
+ return text.replace(nomadnetRegex, (match, hash, path) => {
+ const fullPath = path || "/page/index.mu";
+ const url = `${hash}:${fullPath}`;
+ return `<a href="#" class="nomadnet-link text-blue-600 dark:text-blue-400 hover:underline font-mono" data-nomadnet-url="${url}">${match}</a>`;
+ });
+ }
+
+ /**
+ * Basic URL detection for standard http/https links.
+ */
+ static renderStandardLinks(text) {
+ if (!text) return "";
+
+ // Simple regex for URLs
+ const urlRegex = /(https?:\/\/[^\s<]+)/g;
+ return text.replace(urlRegex, (url) => {
+ return `<a href="${url}" target="_blank" rel="noopener noreferrer" class="text-blue-600 dark:text-blue-400 hover:underline">${url}</a>`;
+ });
+ }
+
+ /**
+ * Applies all link rendering.
+ */
+ static renderAllLinks(text) {
+ text = this.renderStandardLinks(text);
+ text = this.renderNomadNetLinks(text);
+ return text;
+ }
+}

diff --git a/meshchatx/src/frontend/js/MarkdownRenderer.js b/meshchatx/src/frontend/js/MarkdownRenderer.js
new file mode 100644
index 00000000..8da04b84
--- /dev/null
+++ b/meshchatx/src/frontend/js/MarkdownRenderer.js
@@ -0,0 +1,101 @@
+import Utils from "./Utils";
+import LinkUtils from "./LinkUtils";
+
+export default class MarkdownRenderer {
+ /**
+ * A simple Markdown to HTML renderer, cause we dont need another library for this.
+ * Ported and simplified from meshchatx/src/backend/markdown_renderer.py
+ */
+ static render(text) {
+ if (!text) {
+ return "";
+ }
+
+ // Escape HTML entities first to prevent XSS
+ text = Utils.escapeHtml(text);
+
+ // Fenced code blocks - process these FIRST and replace with placeholders
+ const code_blocks = [];
+ text = text.replace(/```(\w+)?\n([\s\S]*?)\n```/g, (match, lang, code) => {
+ const placeholder = `[[CB${code_blocks.length}]]`;
+ code_blocks.push(
+ `<pre class="bg-gray-800 dark:bg-zinc-900 text-zinc-100 dark:text-zinc-100 p-3 rounded-lg my-3 overflow-x-auto border border-gray-700 dark:border-zinc-800 font-mono text-sm"><code class="language-${lang || ""} text-inherit">${code}</code></pre>`
+ );
+ return placeholder;
+ });
+
+ // Headers
+ text = text.replace(/^# (.*)$/gm, '<h1 class="text-xl font-bold mt-4 mb-2"> $1</h1>');
+ text = text.replace(/^## (.*)$/gm, '<h2 class="text-lg font-bold mt-3 mb-1">$1</h2>');
+ text = text.replace(/^### (.*)$/gm, '<h3 class="text-base font-bold mt-2 mb-1">$1</h3>');
+
+ // Bold and Italic
+ text = text.replace(/\*\*\*(.*?)\*\*\*/g, "<strong><em>$1</em></strong>");
+ text = text.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
+ text = text.replace(/\*(.*?)\*/g, "<em>$1</em>");
+ text = text.replace(/___(.*?)___/g, "<strong><em>$1</em></strong>");
+ text = text.replace(/__(.*?)__/g, "<strong>$1</strong>");
+ text = text.replace(/_(.*?)_/g, "<em>$1</em>");
+
+ // Inline code
+ text = text.replace(
+ /`([^`]+)`/g,
+ '<code class="bg-black/10 dark:bg-white/10 px-1 rounded font-mono text-[0.9em]">$1</code>'
+ );
+
+ // Links
+ text = LinkUtils.renderAllLinks(text);
+
+ // Restore code blocks
+ for (let i = 0; i < code_blocks.length; i++) {
+ text = text.replace(`[[CB${i}]]`, code_blocks[i]);
+ }
+
+ // Paragraphs - double newline to p tag
+ const parts = text.split(/\n\n+/);
+ const processed_parts = [];
+ for (let part of parts) {
+ part = part.trim();
+ if (!part) continue;
+
+ // If it's a placeholder for code block, don't wrap in <p>
+ if (part.startsWith("<pre") || part.startsWith("<h")) {
+ processed_parts.push(part);
+ } else {
+ // Replace single newlines with <br> for line breaks within paragraphs
+ part = part.replace(/\n/g, "<br>");
+ processed_parts.push(`<p class="my-2 leading-relaxed">${part}</p>`);
+ }
+ }
+
+ return processed_parts.join("\n");
+ }
+
+ /**
+ * Strips markdown from text for previews.
+ */
+ static strip(text) {
+ if (!text) {
+ return "";
+ }
+
+ // Strip fenced code blocks
+ text = text.replace(/```(\w+)?\n([\s\S]*?)\n```/g, "[Code Block]");
+
+ // Strip headers
+ text = text.replace(/^#+ (.*)$/gm, "$1");
+
+ // Strip bold and italic
+ text = text.replace(/\*\*\*(.*?)\*\*\*/g, "$1");
+ text = text.replace(/\*\*(.*?)\*\*/g, "$1");
+ text = text.replace(/\*(.*?)\*/g, "$1");
+ text = text.replace(/___(.*?)___/g, "$1");
+ text = text.replace(/__(.*?)__/g, "$1");
+ text = text.replace(/_(.*?)_/g, "$1");
+
+ // Strip inline code
+ text = text.replace(/`([^`]+)`/g, "$1");
+
+ return text;
+ }
+}

diff --git a/meshchatx/src/frontend/js/ToastUtils.js b/meshchatx/src/frontend/js/ToastUtils.js
index 600fb32a..ca328d01 100644
--- a/meshchatx/src/frontend/js/ToastUtils.js
+++ b/meshchatx/src/frontend/js/ToastUtils.js
@@ -1,24 +1,28 @@
import GlobalEmitter from "./GlobalEmitter";
class ToastUtils {
- static show(message, type = "info", duration = 5000) {
- GlobalEmitter.emit("toast", { message, type, duration });
+ static show(message, type = "info", duration = 5000, key = null) {
+ GlobalEmitter.emit("toast", { message, type, duration, key });
}
- static success(message, duration = 5000) {
- this.show(message, "success", duration);
+ static success(message, duration = 5000, key = null) {
+ this.show(message, "success", duration, key);
}
- static error(message, duration = 5000) {
- this.show(message, "error", duration);
+ static error(message, duration = 5000, key = null) {
+ this.show(message, "error", duration, key);
}
- static warning(message, duration = 5000) {
- this.show(message, "warning", duration);
+ static warning(message, duration = 5000, key = null) {
+ this.show(message, "warning", duration, key);
}
- static info(message, duration = 5000) {
- this.show(message, "info", duration);
+ static info(message, duration = 5000, key = null) {
+ this.show(message, "info", duration, key);
+ }
+
+ static loading(message, duration = 0, key = null) {
+ this.show(message, "loading", duration, key);
}
}

diff --git a/meshchatx/src/frontend/js/Utils.js b/meshchatx/src/frontend/js/Utils.js
index 125939b6..9dfb99c2 100644
--- a/meshchatx/src/frontend/js/Utils.js
+++ b/meshchatx/src/frontend/js/Utils.js
@@ -183,6 +183,20 @@ class Utils {
const value = rawValue?.toString()?.toLowerCase();
return value === "on" || value === "yes" || value === "true";
}
+
+ static escapeHtml(text) {
+ if (!text) return "";
+ const map = {
+ "&": "&amp;",
+ "<": "&lt;",
+ ">": "&gt;",
+ '"': "&quot;",
+ "'": "&#039;",
+ };
+ return text.replace(/[&<>"']/g, function (m) {
+ return map[m];
+ });
+ }
}
export default Utils;

diff --git a/meshchatx/src/frontend/main.js b/meshchatx/src/frontend/main.js
index b4dcf8ce..9e600724 100644
--- a/meshchatx/src/frontend/main.js
+++ b/meshchatx/src/frontend/main.js
@@ -12,6 +12,7 @@ import App from "./components/App.vue";
import en from "./locales/en.json";
import de from "./locales/de.json";
import ru from "./locales/ru.json";
+import it from "./locales/it.json";
// init i18n
const i18n = createI18n({
@@ -22,6 +23,7 @@ const i18n = createI18n({
en,
de,
ru,
+ it,
},
});
@@ -179,6 +181,11 @@ const router = createRouter({
path: "/rnpath",
component: defineAsyncComponent(() => import("./components/tools/RNPathPage.vue")),
},
+ {
+ name: "rnpath-trace",
+ path: "/rnpath-trace",
+ component: defineAsyncComponent(() => import("./components/tools/RNPathTracePage.vue")),
+ },
{
name: "rnprobe",
path: "/rnprobe",


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────